﻿using Levfier.Proccessors.Parameters;
using System;

namespace Levfier.Proccessors.NoiseReduction
{
    /// <summary>
    /// スペクトラルサブトラクション法によるノイズサプレッサ
    /// </summary>
    [Serializable]
    public class LearningSpectralSubtraction : Proccessor
    {
        /// <summary>
        /// マックスホールドモード
        /// </summary>
        private static readonly int MaxHoldMode = 1;

        /// <summary>
        /// 平均モード
        /// </summary>
        private static readonly int AverageMode = 0;

        /// <summary>
        /// 学習モード
        /// </summary>
        private static readonly int LearningMode = 1;

        /// <summary>
        /// ノイズリダクションモード
        /// </summary>
        private static readonly int ReductionMode = 0;


        /// <summary>
        /// プラス領域のゲインに関するパラメータ
        /// </summary>
        private ProccessParameter reductionParam;

        /// <summary>
        /// 学習フラグ
        /// 0：ノイズリダクションパラメータ
        /// 1：学習
        /// </summary>
        private ProccessParameter learningModeParam;

        /// <summary>
        /// 学習の種類
        /// 0：平均減算
        /// 1：マックスホールド
        /// </summary>
        private ProccessParameter maxHoldModeParam;

        /// <summary>
        /// Binの精度[Hz]
        /// </summary>
        private ProccessParameter accuracyParam;

        /// <summary>
        /// 学習フラグ
        /// 0：ノイズリダクション
        /// 1：学習
        /// </summary>
        int mode;

        /// <summary>
        /// 学習の種類
        /// 0：平均減算
        /// 1：マックスホールド
        /// </summary>
        int maxHold;

        /// <summary>
        /// サンプルカウント
        /// </summary>
        private int sampleCount;

        /// <summary>
        /// FFTフレームのサイズ
        /// 2の冪乗
        /// </summary>
        private int frameSize;

        /// <summary>
        /// フレームサイズの2の対数
        /// </summary>
        private int bitSize;

        /// <summary>
        /// 入力の生データ
        /// </summary>
        private double[] rawData;

        /// <summary>
        /// 入力信号
        /// </summary>
        private double[] inputReal;

        /// <summary>
        /// 入力信号の複素数領域
        /// ダミーとして使う
        /// </summary>
        private double[] inputImage;

        /// <summary>
        /// FFT結果
        /// </summary>
        private double[] outputReal;

        /// <summary>
        /// FFT結果の複素数領域
        /// </summary>
        private double[] outputImage;

        /// <summary>
        /// ノイズリダクション後の信号
        /// </summary>
        private double[][] reulstReal;

        /// <summary>
        /// ノイズリダクション後の信号の複素数領域
        /// </summary>
        private double[][] resultImage;

        /// <summary>
        /// 前回のノイズリダクション後の信号の後半部分
        /// </summary>
        private double[] preResult;

        /// <summary>
        /// 窓関数の係数
        /// フレームサイズ分でなく、サンプルカウント分を確保
        /// </summary>
        private double[] window;

        /// <summary>
        /// 学習用の配列
        /// FFT後
        /// </summary>
        private double[] learningPower;

        /// <summary>
        /// 学習したフレーム数
        /// </summary>
        private int learningCounter;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public LearningSpectralSubtraction()
            : base()
        {
            //サンプルカウントが変化した時のイベントハンドラを登録
            VSTParameterManager.GetInstance().SampleCountChanged += SpectralSubtraction_SampleCountChanged;
            VSTParameterManager.GetInstance().SampleRateChanged += SpectralSubtraction_SampleCountChanged;
        }

        /// <summary>
        /// 音を処理する
        /// </summary>
        /// <param name="inputBuffer">入力</param>
        /// <param name="outputBuffer">出力</param>
        protected override void InnerProccess(double[] inputBuffer, double[] outputBuffer)
        {
            int sampleCountHalf = this.sampleCount / 2;
            double reductionGain = ProccessParameter.ConvertDBToValue(reductionParam.Value);
            //double reductionGain2 = reductionGain * reductionGain;
            double power = 0.0d;
            double powerRate = 0.0d;

            //計算 オーバラップ分繰り返す
            for (int i = 0; i < 2; i++)
            {
                //音を前半にずらすて新しい音を後半に格納
                this.PushInputFrameHalf(inputBuffer, i * sampleCountHalf);

                //生データに窓関数を適用し、格納
                this.ApplyWindow(this.rawData, this.inputReal);

                //フーリエ変換
                CommonMath.FFT(this.inputReal, this.inputImage, out this.outputReal, out this.outputImage, this.bitSize);

                for (int j = 0; j < this.frameSize; j++)
                {
                    //パワースペクトルを算出
                    power = this.outputReal[j] * this.outputReal[j] + this.outputImage[j] * this.outputImage[j];

                    //ノイズリダクションモード
                    if (ReductionMode == this.mode)
                    {
                        if (0 != power)
                        {
                            //学習したノイズパワーを減算する
                            powerRate = (power - this.learningPower[j] * reductionGain) / power;
                        }
                        else
                        {
                            powerRate = 0.0d;
                        }

                        //スペクトルが0以下なら0にする
                        if (0.0 > powerRate)
                        {
                            powerRate = 0.0d;
                        }
                        else if (MaxHoldMode == this.maxHold)
                        {
                            //マックスホールドモードの場合、閾値を超えた周波数はパワーそのまま
                            powerRate = 1.0d;
                        }

                        //スペクトルのレート計算
                        this.outputReal[j] *= powerRate;
                        this.outputImage[j] *= powerRate;
                    }
                    else if(LearningMode == this.mode) //学習モード
                    {
                        if (AverageMode == this.maxHold)
                        {
                            //平均値を計算するため、加算
                            this.learningPower[j] += power;
                        }
                        else if(MaxHoldMode == this.maxHold)
                        {
                            //最大値を保持
                            if(power >  this.learningPower[j])
                            {
                                this.learningPower[j] = power;
                            }
                        }
                    }
                }

                //学習モード時は、学習回数をインクリメント
                if (LearningMode == this.mode)
                {
                    this.learningCounter++;
                }

                CommonMath.IFFT(this.outputReal, this.outputImage, out this.reulstReal[i], out this.resultImage[i], this.bitSize);
            }

            //前半を出力
            for (int i = 0; i < sampleCountHalf; i++)
            {
                outputBuffer[i] = this.reulstReal[0][i] + this.preResult[i];
            }

            //後半を出力
            for (int i = sampleCountHalf; i < this.sampleCount; i++)
            {
                outputBuffer[i] = this.reulstReal[0][i] + this.reulstReal[1][i - sampleCountHalf];
            }

            //次回の計算用に、計算結果の後半をコピーしておく
            for (int i = 0; i < sampleCountHalf; i++)
            {
                this.preResult[i] = this.reulstReal[1][i + sampleCountHalf];
            }
        }

        /// <summary>
        /// パラメータの初期化
        /// </summary>
        protected override void InitParameters()
        {

            this.reductionParam = new ProccessParameter();
            this.reductionParam.Name = "Effect";
            this.reductionParam.Value = 0.0d;
            this.reductionParam.MaxValue = 10.0d;
            this.reductionParam.MinValue = -100.0d;
            this.reductionParam.Step = 1.0d;
            this.parameters.Add(this.reductionParam.Name, this.reductionParam);

            this.learningModeParam = new ProccessParameter();
            this.learningModeParam.Type = ParameterType.Bool;
            this.learningModeParam.Name = "Learning Mode";
            this.learningModeParam.Value = 0.0d;
            this.learningModeParam.MaxValue = 1.0d;
            this.learningModeParam.MinValue = 0.0d;
            this.learningModeParam.Step = 1.0d;
            this.learningModeParam.ValueChanged +=learningModeParam_ValueChanged;
            this.parameters.Add(this.learningModeParam.Name, this.learningModeParam);

            this.maxHoldModeParam = new ProccessParameter();
            this.maxHoldModeParam.Type = ParameterType.Bool;
            this.maxHoldModeParam.Name = "Maxhold Mode";
            this.maxHoldModeParam.Value = 0.0d;
            this.maxHoldModeParam.MaxValue = 1.0d;
            this.maxHoldModeParam.MinValue = 0.0d;
            this.maxHoldModeParam.Step = 1.0d;
            this.maxHoldModeParam.ValueChanged += learningModeParam_ValueChanged;
            this.parameters.Add(this.maxHoldModeParam.Name, this.maxHoldModeParam);

            this.accuracyParam = new ProccessParameter();
            this.accuracyParam.Name = "Accuracy[Hz]";
            this.accuracyParam.Value = 500.0d;
            this.accuracyParam.MaxValue = 1000.0d;
            this.accuracyParam.MinValue = 50.0d;
            this.accuracyParam.Step = 50.0d;
            this.accuracyParam.ValueChanged += accuracyParam_ValueChanged;
            this.parameters.Add(this.accuracyParam.Name, this.accuracyParam);

            this.InitFFT();
        }

        /// <summary>
        /// 学習モード変更フラグ
        /// </summary>
        /// <param name="sender"></param>
        private void learningModeParam_ValueChanged(object sender)
        {
            //学習モード
            this.mode = (int)this.learningModeParam.Value;
            this.maxHold = (int)this.maxHoldModeParam.Value;

            //ノイズリダクションモードの場合
            if (ReductionMode == this.mode)
            {
                if (AverageMode == this.maxHold)
                {
                    for (int i = 0; i < this.learningPower.Length; i++)
                    {
                        //今まで加算した分をカウンタで除算し、平均化
                        this.learningPower[i] = this.learningPower[i] / (double)learningCounter;
                    }
                }
                else if(MaxHoldMode == this.maxHold)
                {
                    //何もしなくてOK
                }
            }
            else if (LearningMode == this.mode) //学習モードになった場合、初期化を行う
            {
                for(int i = 0 ; i < this.learningPower.Length ;i++)
                {
                    this.learningPower[i] = 0.0d;
                }

                //学習カウンタを初期化
                this.learningCounter = 0;
            }
            
        }

        /// <summary>
        /// FFTの初期化
        /// </summary>
        private void InitFFT()
        {
            //周波数精度
            double accuracyFreq = this.accuracyParam.Value;

            double sampleRate = VSTParameterManager.GetInstance().SampleRate;
            this.sampleCount = VSTParameterManager.GetInstance().SampleCount;
            this.frameSize = 1;

            //2の冪乗のフレームサイズを求める
            for (int i = 0; this.frameSize < sampleCount; i++)
            {
                this.frameSize = (int)Math.Pow(2, i);
                this.bitSize = i;
            }

            //設定された周波数精度を満たすフレームサイズを求める
            for (int i = this.bitSize; (sampleRate / (double)this.frameSize) > accuracyFreq; i++)
            {
                this.frameSize = (int)Math.Pow(2, i);
                this.bitSize = i;
            }

            this.rawData = new double[this.sampleCount];

            this.inputReal = new double[this.frameSize];
            this.inputImage = new double[this.frameSize];
            //this.outputReal = new double[this.frameSize];
            //this.outputImage = new double[this.frameSize];

            this.reulstReal = new double[2][];
            this.resultImage = new double[2][];

            //窓関数は実データ部分のみ
            this.window = new double[this.sampleCount];

            this.preResult = new double[this.sampleCount / 2];

            this.learningPower = new double[this.frameSize];

            //窓係数を初期化
            this.InitWindow();
        }

        /// <summary>
        /// サンプルカウントが変化した時の動作
        /// </summary>
        /// <param name="value">変更後の値</param>
        private void SpectralSubtraction_SampleCountChanged(double value)
        {
            this.InitFFT();
        }

        /// <summary>
        /// ビン精度周波数が変更された時の動作
        /// </summary>
        /// <param name="sender"></param>
        private void accuracyParam_ValueChanged(object sender)
        {
            this.InitFFT();
        }

        /// <summary>
        /// 音をフレームに格納する
        /// </summary>
        /// <param name="source">ソース</param>
        /// <param name="startIndex">開始インデックス</param>
        private void PushInputFrameHalf(double[] source, int startIndex)
        {
            int sampleCountHalf = this.sampleCount / 2;

            for (int i = 0; i < sampleCountHalf; i++)
            {
                ////フレームの後半を前半に移動する
                this.rawData[i] = this.rawData[i + sampleCountHalf];

                ////移動して空になった場所に新しく詰め込む
                this.rawData[i + sampleCountHalf] = source[startIndex + i];
            }
        }

        /// <summary>
        /// 窓係数を初期化する
        /// </summary>
        private void InitWindow()
        {
            //窓関数を初期化する
            for (int i = 0; i < this.sampleCount; i++)
            {
                this.window[i] = CommonMath.GetHanningWindowValue(i, this.sampleCount);
            }
        }

        /// <summary>
        /// 窓関数を適用する
        /// </summary>
        private void ApplyWindow(double[] input, double[] output)
        {
            //実データのみに窓関数を適用する
            for (int i = 0; i < this.sampleCount; i++)
            {
                output[i] = this.window[i] * input[i];
            }
        }
    }
}
